﻿using System;
using System.IO;

namespace MetaMapics.Pbf
{
  public static class PbfHelper
  {
    public const uint MaxFieldNum = uint.MaxValue >> 3;

    public static uint ReadVarint(Stream stream)
    {
      uint varint = 0;
      int inputByte;
      int byteCount = 0;
      do
      {
        if (byteCount++ > 5)
          throw new InvalidDataException("Varint is too long");
        inputByte = stream.ReadByte();
        if (inputByte == -1)
          throw new EndOfStreamException();
        varint = varint | ((uint)(inputByte & 0x7f) << ((byteCount - 1) * 7));
      }
      while ((inputByte & 0x80) != 0);
      return varint;
    }

    public static uint ReadVarint(Stream stream, out int byteCount)
    {
      uint varint = 0;
      int inputByte;
      byteCount = 0;
      do
      {
        if (byteCount++ > 5)
          throw new InvalidDataException("Varint is too long");
        inputByte = stream.ReadByte();
        if (inputByte == -1)
          throw new EndOfStreamException();
        varint = varint | ((uint)(inputByte & 0x7f) << ((byteCount - 1) * 7));
      }
      while ((inputByte & 0x80) != 0);
      return varint;
    }

    public static bool TryReadVarint(Stream stream, out uint varint)
    {
      varint = 0;
      int inputByte;
      int byteCount = 0;
      do
      {
        if (byteCount++ > 5)
          throw new InvalidDataException("Varint is too long");
        inputByte = stream.ReadByte();
        if (inputByte == -1 && byteCount == 1)
          return false;
        if (inputByte == -1)
          throw new EndOfStreamException();
        varint = varint | ((uint)(inputByte & 0x7f) << ((byteCount - 1) * 7));
      }
      while ((inputByte & 0x80) != 0);
      return true;
    }

    public static ulong ReadLongVarint(Stream stream)
    {
      ulong varint = 0;
      int inputByte;
      int byteCount = 0;
      do
      {
        if (byteCount++ > 10)
          throw new InvalidDataException("VarInt is too long");
        inputByte = stream.ReadByte();
        if (inputByte == -1)
          throw new EndOfStreamException();
        varint = varint | (((ulong)inputByte & 0x7f) << ((byteCount - 1) * 7));
      }
      while ((inputByte & 0x80) != 0);
      return varint;
    }

    public static ulong ReadLongVarint(Stream stream, out int byteCount)
    {
      ulong varint = 0;
      int inputByte;
      byteCount = 0;
      do
      {
        if (byteCount++ > 10)
          throw new InvalidDataException("Varint is too long");
        inputByte = stream.ReadByte();
        if (inputByte == -1)
          throw new EndOfStreamException();
        varint = varint | ((ulong)(inputByte & 0x7f) << ((byteCount - 1) * 7));
      }
      while ((inputByte & 0x80) != 0);
      return varint;
    }

    public static double ReadDouble(Stream stream)
    {
      var bytes = new byte[8];
      for (int i = 0; i < 8; i++)
      {
        int b = stream.ReadByte();
        if (b == -1)
          throw new EndOfStreamException();
        bytes[i] = (byte)b;
      }
      return BitConverter.ToDouble(bytes, 0);
    }

    public static float ReadFloat(Stream stream)
    {
      var bytes = new byte[4];
      for (int i = 0; i < 4; i++)
      {
        int b = stream.ReadByte();
        if (b == -1)
          throw new EndOfStreamException();
        bytes[i] = (byte)b;
      }
      return BitConverter.ToSingle(bytes, 0);
    }

    public static bool ReadBool(Stream stream)
    {
      uint varint = ReadVarint(stream);
      if (varint == 0)
        return false;
      if (varint == 1)
        return true;
      throw new InvalidDataException();
    }

    public static int ReadBits32(Stream stream)
    {
      var bytes = new byte[4];
      for (int i = 0; i < 4; i++) //?? is this the right way round?
      {
        int b = stream.ReadByte();
        if (b == -1)
          throw new EndOfStreamException();
        bytes[i] = (byte)b;
      }
      return BitConverter.ToInt32(bytes, 0);
    }

    public static long ReadBits64(Stream stream)
    {
      var bytes = new byte[8];
      for (int i = 0; i < 8; i++) //?? is this the right way round?
      {
        int b = stream.ReadByte();
        if (b == -1)
          throw new EndOfStreamException();
        bytes[i] = (byte)b;
      }
      return BitConverter.ToInt64(bytes, 0);
    }

    public static uint ReadTag(Stream stream, out PbfWireType wireType)
    {
      uint varint = ReadVarint(stream);
      wireType = (PbfWireType)(varint & 0x7);
      return varint >> 3;
    }

    public static void WriteVarint(Stream stream, int varint)
    {
      WriteVarint(stream, (uint)varint);
    }

    public static void WriteVarint(Stream stream, long varint)
    {
      WriteVarint(stream, (ulong)varint);
    }

    public static void WriteVarint(Stream stream, ulong varint)
    {
      int byteCount = 0;
      do
      {
        byteCount++;
        byte b = (byte)(varint & 0x7f);
        varint = varint >> 7;
        if (varint != 0)
          b = (byte)(b | 0x80);
        stream.WriteByte(b);
      }
      while (varint != 0);
    }

    public static void WriteTag(Stream stream, uint fieldNum, PbfWireType wireType)
    {
      WriteVarint(stream, (fieldNum << 3) | (uint)wireType);
    }

    public static void WriteDouble(Stream stream, double value)
    {
      byte[] bytes = BitConverter.GetBytes(value);
      for (int i = 0; i < 8; i++) //?? is this the right way round?
        stream.WriteByte(bytes[i]);
    }

    public static void WriteFloat(Stream stream, float value)
    {
      byte[] bytes = BitConverter.GetBytes(value);
      for (int i = 0; i < 4; i++) //?? is this the right way round?
        stream.WriteByte(bytes[i]);
    }

    public static void WriteBool(Stream stream, bool value)
    {
      WriteVarint(stream, (uint)(value ? 1 : 0)); //?? Is this the right way round?
    }

    public static void WriteBits64(Stream stream, long value)
    {
      byte[] bytes = BitConverter.GetBytes(value);
      for (int i = 0; i < 8; i++) //?? is this the right way round?
        stream.WriteByte(bytes[i]);
    }

    public static void WriteBits32(Stream stream, int value)
    {
      byte[] bytes = BitConverter.GetBytes(value);
      for (int i = 0; i < 4; i++) //?? is this the right way round?
        stream.WriteByte(bytes[i]);
    }

    public static long EncodeZigZag(long value)
    {
      return (value << 1) ^ (value >> 63);
    }

    public static int EncodeZigZag(int value)
    {
      return (value << 1) ^ (value >> 31);
    }

    public static long DecodeZigZag(long value)
    {
      return (-(value & 0x01L)) ^ ((value >> 1) & (long)~0x8000000000000000);
    }

    public static int DecodeZigZag(int value)
    {
      return (-(value & 0x01)) ^ ((value >> 1) & (int)~0x80000000);
    }
  }
}
